

<!DOCTYPE html>
<html lang="zh-CN" data-default-color-scheme=auto>



<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/img/fluid.png">
  <link rel="icon" href="/img/fluid.png">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  
  <meta name="theme-color" content="#2f4154">
  <meta name="author" content="叶飞">
  <meta name="keywords" content="">
  
    <meta name="description" content="Redis缓存异常  缓存异常有四种类型，分别是缓存和数据库的数据不一致、缓存雪崩、缓存击穿和缓存穿透   1 缓存和数据库的数据一致性  背景：使用到缓存，无论是本地内存做缓存还是使用 Redis 做缓存，那么就会存在数据同步的问题，因为配置信息缓存在内存中，而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。  共有四种方案：  先更新数据库，后更新缓存">
<meta property="og:type" content="article">
<meta property="og:title" content="redis面试题-2">
<meta property="og:url" content="http://example.com/2022/04/03/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2/index.html">
<meta property="og:site_name" content="博客">
<meta property="og:description" content="Redis缓存异常  缓存异常有四种类型，分别是缓存和数据库的数据不一致、缓存雪崩、缓存击穿和缓存穿透   1 缓存和数据库的数据一致性  背景：使用到缓存，无论是本地内存做缓存还是使用 Redis 做缓存，那么就会存在数据同步的问题，因为配置信息缓存在内存中，而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。  共有四种方案：  先更新数据库，后更新缓存">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb5881bbb1d4tplv-t2oaga2asx-watermark.awebp">
<meta property="og:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb5881a19fectplv-t2oaga2asx-watermark.awebp">
<meta property="og:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb5881fb4a1btplv-t2oaga2asx-watermark.awebp">
<meta property="og:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb588215b298tplv-t2oaga2asx-watermark.awebp">
<meta property="og:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/202105092153018231.png">
<meta property="og:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/202105092153019692.png">
<meta property="og:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/image-20210828175543973.png">
<meta property="article:published_time" content="2022-04-03T09:38:28.000Z">
<meta property="article:modified_time" content="2022-04-03T13:00:57.037Z">
<meta property="article:author" content="叶飞">
<meta property="article:tag" content="面试">
<meta property="article:tag" content="redis">
<meta property="article:tag" content="数据库">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="http://example.com/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb5881bbb1d4tplv-t2oaga2asx-watermark.awebp">
  
  
  
  <title>redis面试题-2 - 博客</title>

  <link  rel="stylesheet" href="https://lib.baomitu.com/twitter-bootstrap/4.6.1/css/bootstrap.min.css" />



  <link  rel="stylesheet" href="https://lib.baomitu.com/github-markdown-css/4.0.0/github-markdown.min.css" />

  <link  rel="stylesheet" href="https://lib.baomitu.com/hint.css/2.7.0/hint.min.css" />

  <link  rel="stylesheet" href="https://lib.baomitu.com/fancybox/3.5.7/jquery.fancybox.min.css" />



<!-- 主题依赖的图标库，不要自行修改 -->
<!-- Do not modify the link that theme dependent icons -->

<link rel="stylesheet" href="//at.alicdn.com/t/font_1749284_hj8rtnfg7um.css">



<link rel="stylesheet" href="//at.alicdn.com/t/font_1736178_lbnruvf0jn.css">


<link  rel="stylesheet" href="/css/main.css" />


  <link id="highlight-css" rel="stylesheet" href="/css/highlight.css" />
  
    <link id="highlight-css-dark" rel="stylesheet" href="/css/highlight-dark.css" />
  




  <script id="fluid-configs">
    var Fluid = window.Fluid || {};
    Fluid.ctx = Object.assign({}, Fluid.ctx)
    var CONFIG = {"hostname":"example.com","root":"/","version":"1.9.2","typing":{"enable":true,"typeSpeed":70,"cursorChar":"_","loop":false,"scope":[]},"anchorjs":{"enable":true,"element":"h1,h2,h3,h4,h5,h6","placement":"left","visible":"hover","icon":""},"progressbar":{"enable":true,"height_px":3,"color":"#29d","options":{"showSpinner":false,"trickleSpeed":100}},"code_language":{"enable":true,"default":"TEXT"},"copy_btn":true,"image_caption":{"enable":true},"image_zoom":{"enable":true,"img_url_replace":["",""]},"toc":{"enable":true,"placement":"right","headingSelector":"h1,h2,h3,h4,h5,h6","collapseDepth":0},"lazyload":{"enable":true,"loading_img":"/img/loading.gif","onlypost":false,"offset_factor":2},"web_analytics":{"enable":false,"follow_dnt":true,"baidu":null,"google":null,"gtag":null,"tencent":{"sid":null,"cid":null},"woyaola":null,"cnzz":null,"leancloud":{"app_id":null,"app_key":null,"server_url":null,"path":"window.location.pathname","ignore_local":false}},"search_path":"/local-search.xml"};

    if (CONFIG.web_analytics.follow_dnt) {
      var dntVal = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
      Fluid.ctx.dnt = dntVal && (dntVal.startsWith('1') || dntVal.startsWith('yes') || dntVal.startsWith('on'));
    }
  </script>
  <script  src="/js/utils.js" ></script>
  <script  src="/js/color-schema.js" ></script>
  


  
<meta name="generator" content="Hexo 6.2.0"></head>


<body>
  

  <header>
    

<div class="header-inner" style="height: 70vh;">
  <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">
    <a class="navbar-brand" href="/">
      <strong>No Tomorrow</strong>
    </a>

    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/">
                <i class="iconfont icon-home-fill"></i>
                首页
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/archives/">
                <i class="iconfont icon-archive-fill"></i>
                归档
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/categories/">
                <i class="iconfont icon-category-fill"></i>
                分类
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/tags/">
                <i class="iconfont icon-tags-fill"></i>
                标签
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/about/">
                <i class="iconfont icon-user-fill"></i>
                关于
              </a>
            </li>
          
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" target="_self" href="javascript:;" data-toggle="modal" data-target="#modalSearch" aria-label="Search">
              &nbsp;<i class="iconfont icon-search"></i>&nbsp;
            </a>
          </li>
          
        
        
          <li class="nav-item" id="color-toggle-btn">
            <a class="nav-link" target="_self" href="javascript:;" aria-label="Color Toggle">&nbsp;<i
                class="iconfont icon-dark" id="color-toggle-icon"></i>&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>
</nav>

  

<div id="banner" class="banner" parallax=true
     style="background: url('/img/default.png') no-repeat center center; background-size: cover;">
  <div class="full-bg-img">
    <div class="mask flex-center" style="background-color: rgba(0, 0, 0, 0.3)">
      <div class="banner-text text-center fade-in-up">
        <div class="h2">
          
            <span id="subtitle" data-typed-text="redis面试题-2"></span>
          
        </div>

        
          
  <div class="mt-3">
    
      <span class="post-meta mr-2">
        <i class="iconfont icon-author" aria-hidden="true"></i>
        叶飞
      </span>
    
    
      <span class="post-meta">
        <i class="iconfont icon-date-fill" aria-hidden="true"></i>
        <time datetime="2022-04-03 17:38" pubdate>
          星期日, 四月 3日 2022, 5:38 下午
        </time>
      </span>
    
  </div>

  <div class="mt-1">
    
      <span class="post-meta mr-2">
        <i class="iconfont icon-chart"></i>
        
          <!-- compatible with older versions-->
          7.9k 字
        
      </span>
    

    
      <span class="post-meta mr-2">
        <i class="iconfont icon-clock-fill"></i>
        
        
        
          <!-- compatible with older versions-->
          67 分钟
        
      </span>
    

    
    
      
        <span id="leancloud-page-views-container" class="post-meta" style="display: none">
          <i class="iconfont icon-eye" aria-hidden="true"></i>
          <span id="leancloud-page-views"></span> 次
        </span>
        
      
    
  </div>


        
      </div>

      
    </div>
  </div>
</div>

</div>

  </header>

  <main>
    
      

<div class="container-fluid nopadding-x">
  <div class="row nomargin-x">
    <div class="side-col d-none d-lg-block col-lg-2">
      

    </div>

    <div class="col-lg-8 nopadding-x-md">
      <div class="container nopadding-x-md" id="board-ctn">
        <div id="board">
          <article class="post-content mx-auto">
            <!-- SEO header -->
            <h1 style="display: none">redis面试题-2</h1>
            
            
              <div class="markdown-body">
                
                <h1 id="redis缓存异常"><a class="markdownIt-Anchor" href="#redis缓存异常"></a> Redis缓存异常</h1>
<blockquote>
<p><strong>缓存异常有四种类型，分别是缓存和数据库的数据不一致、缓存雪崩、缓存击穿和缓存穿透</strong></p>
</blockquote>
<h2 id="1-缓存和数据库的数据一致性"><a class="markdownIt-Anchor" href="#1-缓存和数据库的数据一致性"></a> 1 缓存和数据库的数据一致性</h2>
<blockquote>
<p>背景：使用到缓存，无论是本地内存做缓存还是使用 Redis 做缓存，那么就会存在数据同步的问题，因为配置信息缓存在内存中，而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。</p>
</blockquote>
<p>共有四种方案：</p>
<ol>
<li>先<code>更新</code>数据库，后更新缓存</li>
<li>先更新<code>缓存</code>，后更新数据库</li>
<li>先<code>删除缓存</code>，后更新数据库</li>
<li>先更新数据库，后删除缓存</li>
</ol>
<p>第一种和第二种方案，没有人使用的，因为第一种方案存在问题是：并发更新数据库场景下，会将脏数据刷到缓存。</p>
<p>第二种方案存在的问题是：如果先更新缓存成功，但是数据库更新失败，则肯定会造成数据不一致。</p>
<p>目前主要用第三和第四种方案。</p>
<h2 id="2-先删除缓存后更新数据库"><a class="markdownIt-Anchor" href="#2-先删除缓存后更新数据库"></a> 2 先删除缓存，后更新数据库</h2>
<h3 id="21-问题"><a class="markdownIt-Anchor" href="#21-问题"></a> 2.1 问题:</h3>
<p>请求 A（更新操作） 和请求 B（查询操作）</p>
<ol>
<li>请求A进行写操作，删除缓存</li>
<li>请求B查询发现缓存不存在</li>
<li>请求B去数据库查询得到旧值</li>
<li>请求B将旧值写入缓存</li>
<li>请求A将新值写入数据库</li>
</ol>
<p>上述情况就会导致不一致的情形出现。而且，如果不采用给缓存设置过期时间策略，该数据永远都是脏数据。</p>
<h3 id="22-解决方案"><a class="markdownIt-Anchor" href="#22-解决方案"></a> 2.2 解决方案:</h3>
<h4 id="延时双删"><a class="markdownIt-Anchor" href="#延时双删"></a> 延时双删</h4>
<p>使用伪代码如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">write</span><span class="hljs-params">(String key,Object data)</span>&#123;<br>        Redis.delKey(key);<br>        db.updateData(data);<br>        Thread.sleep(<span class="hljs-number">1000</span>);<br>    	<span class="hljs-comment">// 延时 再次删除缓存</span><br>        Redis.delKey(key);<br>    &#125;<br></code></pre></td></tr></table></figure>
<p>转化为中文描述就是 （1）先淘汰缓存 （2）再写数据库（这两步和原来一样） （3）休眠1秒，再次淘汰缓存，这么做，可以将1秒内所造成的缓存脏数据，再次删除。确保读请求结束，写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时，写数据的休眠时间则在读数据业务逻辑的耗时基础上，加几百ms即可。</p>
<p>如果使用的是 Mysql 的读写分离的架构的话，那么其实主从同步之间也会有时间差。</p>
<p><img src="/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb5881bbb1d4tplv-t2oaga2asx-watermark.awebp" srcset="/img/loading.gif" lazyload alt="主从同步时间差"></p>
<p>此时来了两个请求，请求 A（更新操作） 和请求 B（查询操作）</p>
<ol>
<li>请求 A 更新操作，删除了 Redis</li>
<li>请求主库进行更新操作，主库与从库进行同步数据的操作</li>
<li>请 B 查询操作，发现 Redis 中没有数据</li>
<li>去从库中拿去数据</li>
<li>此时同步数据还未完成，拿到的数据是旧数据</li>
</ol>
<p>此时的<code>解决办法</code>就是如果是对 Redis 进行填充数据的查询数据库操作，那么就<code>强制将其指向主库</code>进行查询。</p>
<p><img src="/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb5881a19fectplv-t2oaga2asx-watermark.awebp" srcset="/img/loading.gif" lazyload alt="从主库中拿数据"></p>
<h4 id="更新与读取操作进行异步串行化"><a class="markdownIt-Anchor" href="#更新与读取操作进行异步串行化"></a> 更新与读取操作进行异步串行化</h4>
<p>采用<strong>更新与读取操作进行异步串行化</strong></p>
<p><strong>异步串行化</strong></p>
<p>我在系统内部维护n个内存<code>队列</code>，更新数据的时候，根据<code>数据的唯一标识</code>，将该操作路由之后，发送到其中一个jvm内部的内存队列中（<strong>对同一数据的请求发送到同一个队列</strong>）。<code>读取数据</code>的时候，如果发现数据不在缓存中，并且此时队列里有更新库存的操作，那么将重新发起<code>读取数据+更新缓存</code>的操作，根据唯一标识路由之后，(操作)也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程，每个工作线程<code>串行</code>地拿到对应的操作，然后一条一条的执行。</p>
<p>这样的话，一个数据变更的操作，先执行删除缓存，然后再去更新数据库，但是还没完成更新的时候，如果此时一个读请求过来，读到了空的缓存，那么可以先将<code>缓存更新的请求发送到队列</code>中，此时会在队列中积压，排在刚才更新库的操作之后，然后<code>同步等待</code>缓存更新完成，再读库。</p>
<p><strong>读操作去重</strong></p>
<p>多个读库更新缓存的请求串在同一个队列中是没意义的，因此可以<code>做过滤</code>，如果发现队列中已经有了该数据的更新缓存的请求了，那么就不用再放进去了，直接等待前面的更新操作请求完成即可，待那个队列对应的工作线程完成了上一个操作（数据库的修改）之后，才会去执行下一个操作（读库更新缓存），此时会从数据库中读取最新的值，然后写入缓存中。</p>
<p>如果请求还在等待时间范围内，不断轮询发现可以取到值了，那么就直接返回；如果<strong>请求等待的时间超过一定时长(为了用户体验)</strong>，那么这一次直接从数据库中读取当前的旧值。（返回旧值不是又导致缓存和数据库不一致了么？那至少可以减少这个情况发生，因为等待超时也不是每次都是，几率很小吧。这里我想的是，如果超时了就直接读旧值，这时候<strong>仅仅是读库后返回而不放缓存</strong>）</p>
<h2 id="3-先更新数据库后删除缓存"><a class="markdownIt-Anchor" href="#3-先更新数据库后删除缓存"></a> 3 先更新数据库，后删除缓存</h2>
<h3 id="31-问题"><a class="markdownIt-Anchor" href="#31-问题"></a> 3.1 问题</h3>
<p>比如更新数据库成功了，但是在删除缓存的阶段出错了没有删除成功，那么此时再读取缓存的时候每次都是错误的数据了。</p>
<p><img src="/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb5881fb4a1btplv-t2oaga2asx-watermark.awebp" srcset="/img/loading.gif" lazyload alt="先更新数据库，后删除缓存"></p>
<h3 id="32-解决方案"><a class="markdownIt-Anchor" href="#32-解决方案"></a> 3.2 解决方案</h3>
<p>利用<strong>消息队列</strong>进行删除的补偿。具体的业务逻辑用语言描述如下：</p>
<ol>
<li>请求 A 先对数据库进行更新操作</li>
<li>在对 Redis 进行删除操作的时候发现报错，删除失败</li>
<li>此时将Redis 的 key 作为消息体发送到消息队列中</li>
<li>系统接收到消息队列发送的消息后<strong>再次</strong>对 Redis 进行删除操作</li>
</ol>
<p>但是这个方案会有一个<strong>缺点</strong>就是会对业务代码造成大量的侵入，深深的耦合在一起.</p>
<p>所以这时会有一个优化的方案，我们知道对 Mysql 数据库更新操作后在 <code>binlog</code> 日志中我们都能够找到相应的操作，那么我们可以<code>订阅</code> Mysql 数据库的 <code>binlog</code> 日志对缓存进行操作。</p>
<ul>
<li>订阅:广播</li>
</ul>
<p><img src="/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/1735bb588215b298tplv-t2oaga2asx-watermark.awebp" srcset="/img/loading.gif" lazyload alt="利用订阅 binlog 删除缓存"></p>
<h2 id="4-什么是缓存雪崩大规模key失效"><a class="markdownIt-Anchor" href="#4-什么是缓存雪崩大规模key失效"></a> 4 什么是缓存雪崩?–大规模key失效</h2>
<blockquote>
<p>某一个时刻出现<strong>大规模</strong>的key失效，那么就会导致大量的请求打在了数据库上面，导致数据库压力巨大，如果在高并发的情况下，可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库，马上又会有新的流量把数据库打死。这就是缓存雪崩。</p>
</blockquote>
<h3 id="41-原因同一时间的大规模的key失效"><a class="markdownIt-Anchor" href="#41-原因同一时间的大规模的key失效"></a> 4.1 原因：同一时间的大规模的key失效</h3>
<ul>
<li>第一种是Redis宕机；</li>
<li>第二种可能就是采用了相同的过期时间。</li>
</ul>
<h3 id="42-解决方案"><a class="markdownIt-Anchor" href="#42-解决方案"></a> 4.2 解决方案</h3>
<p>1、事前：</p>
<ul>
<li><strong>均匀过期</strong>：设置不同的过期时间，让缓存失效的时间尽量均匀，避免相同的过期时间导致缓存雪崩，造成大量数据库的访问。如把每个Key的失效时间都加个随机值，<code>setRedis（Key，value，time + Math.random() * 10000）；</code>，保证数据不会在同一时间大面积失效。</li>
<li><strong>分级缓存</strong>：第一级缓存失效的基础上，访问二级缓存，每一级缓存的失效时间都不同。</li>
<li><strong>热点数据</strong>缓存<strong>永远不过期</strong>。永不过期实际包含两层意思：
<ul>
<li>物理不过期，针对热点key不设置过期时间</li>
<li>逻辑过期，把过期时间存在key对应的value里，如果发现要过期了，通过一个后台的异步线程进行缓存的构建</li>
</ul>
</li>
<li>保证Redis缓存的高可用，防止<strong>Redis宕机</strong>导致缓存雪崩的问题。可以使用 <strong>主从+ 哨兵</strong>，<strong>Redis集群</strong>来避免 Redis 全盘崩溃的情况。</li>
</ul>
<p>2、事中：</p>
<ul>
<li><strong>互斥锁</strong>：<strong>在缓存失效后</strong>，通过互斥锁或者队列来<strong>控制读数据写缓存的线程数量</strong>，比如某个key只允许一个线程查询数据和写缓存，其他线程等待。这种方式会阻塞其他的线程，此时系统的吞吐量会下降</li>
<li>使用<strong>熔断机制，限流降级</strong>。当流量达到一定的<strong>阈值</strong>，直接返回“系统拥挤”之类的提示，防止过多的请求打在数据库上将数据库击垮，至少能保证一部分用户是可以正常使用，其他用户多刷新几次也能得到结果。</li>
</ul>
<p>3、事后：</p>
<p>开启<strong>Redis持久化机制</strong>，尽快恢复缓存数据，一旦重启，就能从磁盘上自动加载数据恢复内存中的数据。</p>
<h2 id="5-什么是缓存击穿热点key失效"><a class="markdownIt-Anchor" href="#5-什么是缓存击穿热点key失效"></a> 5 什么是缓存击穿?–热点key失效</h2>
<blockquote>
<p>缓存击穿是某个<strong>热点的key</strong>失效，大并发集中对其进行请求，就会造成大量请求读缓存没读到数据，从而导致高并发访问<strong>数据库</strong>，引起数据库压力剧增。这种现象就叫做缓存击穿。</p>
</blockquote>
<h3 id="51-解决方案"><a class="markdownIt-Anchor" href="#51-解决方案"></a> 5.1 解决方案</h3>
<p>从两个方面解决，第一是否可以考虑热点key不设置过期时间，第二是否可以考虑降低打在数据库上的请求数量。</p>
<p>解决方案：</p>
<ul>
<li>在<strong>缓存失效后</strong>，通过<strong>互斥锁或者队列</strong>来控制读数据写缓存的线程数量，比如某个key只允许一个线程查询数据和写缓存，其他线程等待。这种方式会阻塞其他的线程，此时系统的吞吐量会下降</li>
<li><strong>热点数据缓存永远不过期</strong>。永不过期实际包含两层意思：
<ul>
<li>物理不过期，针对热点key不设置过期时间</li>
<li>逻辑过期，把过期时间存在key对应的value里，如果发现要过期了，通过一个后台的异步线程进行缓存的构建</li>
</ul>
</li>
</ul>
<h2 id="6-什么是缓存穿透不存在的key"><a class="markdownIt-Anchor" href="#6-什么是缓存穿透不存在的key"></a> 6 什么是缓存穿透?–不存在的key</h2>
<blockquote>
<p>缓存穿透是指用户请求的数据在<strong>缓存</strong>中<strong>不存在</strong>即没有命中，同时在<strong>数据库</strong>中也<strong>不存在</strong>，导致用户每次请求该数据都要去数据库中查询一遍。</p>
<p>如果有恶意攻击者不断请求系统中不存在的数据，会导致短时间大量请求落在数据库上，造成数据库压力过大，甚至导致数据库承受不住而宕机崩溃。</p>
</blockquote>
<h3 id="61-解决方案"><a class="markdownIt-Anchor" href="#61-解决方案"></a> 6.1 解决方案</h3>
<ul>
<li>将<strong>无效的key</strong>存放进 Redis 中：</li>
</ul>
<p>当出现 Redis 查不到数据，数据库也查不到数据的情况，我们就把这个key保存到Redis中，设置value=“null”，并设置其<strong>过期时间极短</strong>，后面再出现查询这个key的请求的时候，直接返回null，就不需要再查询数据库了。</p>
<p>但这种处理方式是有<strong>问题</strong>的，假如传进来的这个不存在的Key值每次都是随机的，那存进Redis也没有意义。</p>
<p>​	- 缓存大量不存在的key</p>
<ul>
<li>使用<strong>布隆过滤器</strong>：【还是哈希映射的思路】</li>
</ul>
<p>如果布隆过滤器判定某个 key 不存在布隆过滤器中，那么就一定不存在，如果判定某个 key 存在，那么很大可能是存在(<strong>存在一定的误判率</strong>)。于是我们可以在缓存之前再加一个布隆过滤器，将数据库中的所有key都存储在布隆过滤器中，在查询Redis前先去布隆过滤器查询 key 是否存在，如果不存在就直接返回，不让其访问数据库，从而避免了对底层存储系统的查询压力。</p>
<blockquote>
<p>如何选择：针对一些恶意攻击，攻击带过来的大量key是随机，那么我们采用第一种方案就会缓存大量不存在key的数据。那么这种方案就不合适了，我们可以先对使用布隆过滤器方案进行过滤掉这些key。</p>
<p>所以，针对这种<strong>key异常多、请求重复率比较低</strong>的数据，优先使用第二种方案直接过滤掉。而对于<strong>空数据的key有限的，重复率比较高</strong>的，则可优先采用第一种方式进行缓存。</p>
</blockquote>
<h2 id="7-什么是缓存预热"><a class="markdownIt-Anchor" href="#7-什么是缓存预热"></a> 7 什么是缓存预热?</h2>
<blockquote>
<p>缓存预热是指系统上线后，<strong>提前将相关的缓存数据</strong>加载到缓存系统。避免在用户请求的时候，先查询数据库，然后再将数据缓存的问题，用户直接查询事先被预热的缓存数据。</p>
</blockquote>
<p>如果不进行预热，那么Redis初始状态数据为空，系统<strong>上线初期</strong>，对于高并发的流量，都会访问到数据库中， 对数据库造成流量的压力。</p>
<p>缓存预热解决方案：</p>
<ul>
<li><strong>数据量不大</strong>的时候，工程启动的时候进行加载缓存动作；</li>
<li><strong>数据量大</strong>的时候，设置一个定时任务脚本，进行缓存的刷新；</li>
<li><strong>数据量太大</strong>的时候，优先保证<strong>热点数据</strong>进行提前加载到缓存。</li>
</ul>
<h2 id="8-什么是缓存降级"><a class="markdownIt-Anchor" href="#8-什么是缓存降级"></a> 8 什么是缓存降级？</h2>
<blockquote>
<p>缓存降级是指缓存失效或缓存服务器挂掉的情况下，不去访问数据库，直接返回<strong>默认数据或访问服务的内存数据</strong>。降级一般是有损的操作，所以尽量减少降级对于业务的影响程度。</p>
</blockquote>
<p>在进行降级之前要对系统进行梳理，看看系统是不是可以丢卒保帅；从而梳理出哪些必须誓死保护，哪些可降级；比如可以<strong>参考日志级别设置预案</strong>：</p>
<ul>
<li>一般：比如有些服务偶尔因为网络抖动或者服务正在上线而超时，可以自动降级；</li>
<li>警告：有些服务在一段时间内成功率有波动（如在95~100%之间），可以自动降级或人工降级，并发送告警；</li>
<li>错误：比如可用率低于90%，或者数据库连接池被打爆了，或者访问量突然猛增到系统能承受的最大阀值，此时可以根据情况自动降级或者人工降级；</li>
<li>严重错误：比如因为特殊原因数据错误了，此时需要紧急人工降级。</li>
</ul>
<h1 id="线程模型"><a class="markdownIt-Anchor" href="#线程模型"></a> 线程模型</h1>
<h2 id="1-redis为何选择单线程"><a class="markdownIt-Anchor" href="#1-redis为何选择单线程"></a> 1 Redis为何选择单线程？</h2>
<p>在Redis 6.0以前，Redis的核心网络模型选择用单线程来实现。</p>
<p>对于一个 DB 来说，CPU 通常不会是瓶颈，因为大多数请求不会是 CPU 密集型的，而是 I/O 密集型。具体到 Redis的话，如果不考虑 RDB/AOF 等持久化方案，Redis是完全的纯内存操作，执行速度是非常快的，因此这部分操作通常不会是性能瓶颈，<strong>Redis真正的性能瓶颈在于网络 I/O</strong>，也就是客户端和服务端之间的网络传输延迟，因此 Redis选择了<strong>单线程的 I/O 多路复用</strong>来实现它的核心网络模型。</p>
<p>实际上更加具体的选择单线程的原因如下：</p>
<ul>
<li><strong>避免过多的上下文切换开销</strong>：如果是单线程则可以规避进程内频繁的线程切换开销，因为程序始终运行在进程中单个线程内，没有多线程切换的场景。</li>
<li><strong>避免同步机制的开销</strong>：如果 Redis选择多线程模型，又因为 Redis是一个数据库，那么势必涉及到底层数据同步的问题，则必然会引入某些同步机制，比如锁，而我们知道 Redis不仅仅提供了简单的 key-value 数据结构，还有 list、set 和 hash 等等其他丰富的数据结构，而不同的数据结构对同步访问的加锁粒度又不尽相同，可能会导致在操作数据过程中带来很多加锁解锁的开销，增加程序复杂度的同时还会降低性能。</li>
<li><strong>简单可维护</strong>：如果 Redis使用多线程模式，那么所有的底层数据结构都必须实现成线程安全的，这无疑又使得 Redis的实现变得更加复杂。</li>
</ul>
<p>总而言之，Redis选择单线程可以说是多方博弈之后的一种权衡：在保证足够的性能表现之下，使用单线程保持代码的简单和可维护性。</p>
<h2 id="2-redis真的是单线程"><a class="markdownIt-Anchor" href="#2-redis真的是单线程"></a> 2 Redis真的是单线程？</h2>
<p>讨论 这个问题前，先看下 Redis的版本中两个重要的节点：</p>
<ol>
<li>Redisv4.0（引入多线程处理异步任务）</li>
<li>Redis 6.0（在网络模型中实现<strong>多线程 I/O</strong> ）</li>
</ol>
<p>所以，网络上说的Redis是单线程，通常是指在Redis 6.0之前，其核心网络模型使用的是单线程。</p>
<p>且Redis6.0引入<strong>多线程I/O</strong>，只是用来<strong>处理网络数据的读写和协议的解析</strong>，而<strong>执行命令依旧是单线程</strong>。</p>
<blockquote>
<p>Redis在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作，此举主要针对的是那些非常耗时的命令，通过将这些命令的执行进行异步化，避免阻塞单线程的事件循环。</p>
<p>在 Redisv4.0 之后增加了一些的非阻塞命令如 <code>UNLINK</code>、<code>FLUSHALL ASYNC</code>、<code>FLUSHDB ASYNC</code>。</p>
</blockquote>
<h2 id="3-redis-60为何引入多线程"><a class="markdownIt-Anchor" href="#3-redis-60为何引入多线程"></a> 3 Redis 6.0为何引入多线程？</h2>
<p>很简单，就是 <strong>Redis的网络 I/O 瓶颈已经越来越明显了</strong>。</p>
<p>随着互联网的飞速发展，互联网业务系统所要处理的线上流量越来越大，Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量，要提升 Redis的性能有两个方向：</p>
<ul>
<li>优化网络 I/O 模块</li>
<li>提高机器内存读写的速度</li>
</ul>
<p>后者依赖于硬件的发展，暂时无解。所以只能从前者下手，网络 I/O 的优化又可以分为两个方向：</p>
<ul>
<li>零拷贝技术或者 DPDK 技术</li>
<li>利用多核优势</li>
</ul>
<p><strong>零拷贝技术</strong>有其局限性，无法完全适配 Redis这一类复杂的网络 I/O 场景，更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术。而 <strong>DPDK 技术</strong>通过旁路网卡 I/O 绕过内核协议栈的方式又<u>太过于复杂</u>以及需要内核甚至是硬件的支持。</p>
<p>总结起来，Redis支持多线程主要就是两个原因：</p>
<ul>
<li>可以充分利用服务器 CPU 资源，目前主线程只能利用一个核</li>
<li>多线程任务可以分摊 Redis 同步 IO 读写负荷</li>
</ul>
<h2 id="4-多线程性能提升效果"><a class="markdownIt-Anchor" href="#4-多线程性能提升效果"></a> 4 多线程性能提升效果</h2>
<p>Redis 作者 antirez 在 RedisConf 2019 分享时曾提到：Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。</p>
<p>国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试，GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了。</p>
<h2 id="5-介绍下redis的线程模型"><a class="markdownIt-Anchor" href="#5-介绍下redis的线程模型"></a> 5 介绍下Redis的线程模型</h2>
<h3 id="51-redis-60之前"><a class="markdownIt-Anchor" href="#51-redis-60之前"></a> 5.1 Redis 6.0之前</h3>
<p>​	Redis 是基于 reactor 模式开发了网络事件处理器，这个处理器叫做文件事件处理器（file event handler）。由于这个文件事件处理器是单线程的，所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket，根据 socket 上的事件来选择对应的事件处理器来处理这个事件。</p>
<blockquote>
<p>IO多路复用是 IO 模型的一种，有时也称为<strong>异步阻塞 IO</strong>，是基于经典的 Reactor 设计模式设计的。<strong>多路</strong>指的是多个 Socket 连接，<strong>复用</strong>指的是复用一个线程。多路复用主要有三种技术：Select，Poll，Epoll。</p>
<p>Epoll 是最新的也是目前最好的多路复用技术。</p>
</blockquote>
<p>模型如下图：</p>
<p><img src="/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/202105092153018231.png" srcset="/img/loading.gif" lazyload alt="202105092153018231.png"></p>
<p>文件事件处理器的结构包含了四个部分：</p>
<ul>
<li>
<p><strong>多个 Socket</strong>。Socket 会产生 AE_READABLE 和 AE_WRITABLE 事件：</p>
<ul>
<li>当 socket 变得可读时或者有新的可以应答的 socket 出现时，socket 就会产生一个 <strong>AE_READABLE</strong> 事件</li>
<li>当 socket 变得可写时，socket 就会产生一个 <strong>AE_WRITABLE</strong> 事件。</li>
</ul>
</li>
<li>
<p><strong>IO 多路复用程序</strong></p>
</li>
<li>
<p><strong>文件事件分派器</strong></p>
</li>
<li>
<p><strong>事件处理器</strong>。事件处理器包括：<code>连接应答处理器</code>、<code>命令请求处理器</code>、<code>命令回复处理器</code>，每个处理器对应不同的 socket 事件：</p>
<ul>
<li>如果是客户端要<code>连接 Redis</code>，那么会为 socket 关联<code>连接应答处理器</code></li>
<li>如果是客户端要<code>写数据</code>到 Redis（读、写请求命令），那么会为 socket 关联<code>命令请求处理器</code></li>
<li>如果是客户端要从 Redis <code>读数据</code>，那么会为 socket 关联<code>命令回复处理器</code></li>
</ul>
<p>多个 socket 会产生不同的事件，不同的事件对应着不同的操作，IO 多路复用程序监听着这些 Socket，当这些 Socket 产生了事件，IO 多路复用程序会将这些事件放到一个队列中，通过这个队列，以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后，IO 多路复用程序才会继续向文件分派器传送下一个事件。</p>
<p>图是客户端与 Redis 通信的一次完整的流程：</p>
</li>
</ul>
<p><img src="/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/202105092153019692.png" srcset="/img/loading.gif" lazyload alt="202105092153019692.png"></p>
<ol>
<li>Redis 启动初始化的时候，Redis 会将<code>连接应答处理器</code>与 <code>AE_READABLE</code> 事件关联起来。</li>
<li>如果一个客户端跟 Redis 发起连接，此时 Redis 会产生一个 AE_READABLE 事件，由于开始之初 AE_READABLE 是与连接应答处理器关联，所以由连接应答处理器来处理该事件，这时连接应答处理器会与客户端建立连接，创建<strong>客户端响应的 socket</strong>，同时将这个 socket 的 AE_READABLE 事件与<code>命令请求处理器</code>关联起来。</li>
<li>如果这个时间客户端向 Redis 发送一个命令（set k1 v1），这时 socket 会产生一个 AE_READABLE 事件，IO 多路复用程序会将该事件压入队列中，此时事件分派器从队列中取得该事件，由于该 socket 的 AE_READABLE 事件已经和<code>命令请求处理器</code>关联了，因此事件分派器会将该事件交给<code>命令请求处理器</code>处理，命令请求处理器读取事件中的命令并完成。操作完成后，Redis 会将该 socket 的 AE_WRITABLE 事件与<code>命令回复处理器</code>关联。</li>
<li>如果客户端已经准备好接受数据后，Redis 中的该 socket 会产生一个 AE_WRITABLE 事件，同样会压入队列然后被事件派发器取出交给相对应的<code>命令回复处理器</code>，由该命令回复处理器将准备好的响应数据写入 socket 中，供客户端读取。</li>
<li>命令回复处理器写完后，就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的<code>关联关系</code>。</li>
</ol>
<h2 id="6-redis-60-多线程的实现机制"><a class="markdownIt-Anchor" href="#6-redis-60-多线程的实现机制"></a> 6 Redis 6.0 多线程的实现机制？</h2>
<p><strong>流程简述如下</strong>：</p>
<ul>
<li>主线程负责接收建立连接请求，获取 Socket 放入<code>全局等待读处理队列</code>。</li>
<li>主线程处理完读事件之后，通过 <code>RR（Round Robin</code>）将这些连接分配给这些 <strong>IO 线程。</strong></li>
<li>主线程阻塞等待 IO 线程读取 Socket 完毕。</li>
<li><strong>主线程通过单线程的方式</strong>执行请求命令，请求数据读取并解析完成，但并不执行。</li>
<li>主线程阻塞等待 IO 线程将数据回写 Socket 完毕。</li>
</ul>
<p><img src="/img/redis%E9%9D%A2%E8%AF%95%E9%A2%98-2.assets/image-20210828175543973.png" srcset="/img/loading.gif" lazyload alt="image-20210828175543973"></p>
<p><strong>该设计有如下特点</strong>：</p>
<ul>
<li>IO 线程要么同时在读 Socket，要么同时在写，不会同时读或写。</li>
<li>IO 线程只负责读写 Socket 解析命令，不负责命令处理。</li>
</ul>
<h2 id="7-redis-60开启多线程后是否会存在线程并发安全问题"><a class="markdownIt-Anchor" href="#7-redis-60开启多线程后是否会存在线程并发安全问题"></a> 7 Redis 6.0开启多线程后，是否会存在线程并发安全问题？</h2>
<p>从实现机制可以看出，Redis 的<strong>多线程部分</strong>只是用来处理<strong>网络数据的读写和协议解析</strong>，执行命令仍然是单线程顺序执行。</p>
<p>所以我们不需要去考虑控制 Key、Lua、事务，LPUSH/LPOP 等等的并发及线程安全问题。</p>
<h2 id="8-redis-60-与-memcached-多线程模型的对比"><a class="markdownIt-Anchor" href="#8-redis-60-与-memcached-多线程模型的对比"></a> 8 Redis 6.0 与 Memcached 多线程模型的对比</h2>
<ul>
<li>
<p>**相同点：**都采用了 Master 线程 -Worker 线程的模型。</p>
</li>
<li>
<p><strong>不同点</strong>：Memcached 执行主逻辑也是在 Worker 线程里，模型更加简单，实现了真正的线程隔离，符合我们对线程隔离的常规理解。</p>
<p>而 Redis 把处理逻辑交还给 Master 线程，虽然一定程度上增加了模型复杂度，但也解决了线程并发安全等问题。</p>
</li>
</ul>

                
              </div>
            
            <hr/>
            <div>
              <div class="post-metas my-3">
  
    <div class="post-meta mr-3 d-flex align-items-center">
      <i class="iconfont icon-category"></i>
      

<span class="category-chains">
  
  
    
      <span class="category-chain">
        
  <a href="/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/" class="category-chain-item">数据库</a>
  
  
    <span>></span>
    
  <a href="/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/redis/" class="category-chain-item">redis</a>
  
  

  

      </span>
    
  
</span>

    </div>
  
  
    <div class="post-meta">
      <i class="iconfont icon-tags"></i>
      
        <a href="/tags/%E9%9D%A2%E8%AF%95/">#面试</a>
      
        <a href="/tags/redis/">#redis</a>
      
        <a href="/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/">#数据库</a>
      
    </div>
  
</div>


              
  

  <div class="license-box my-3">
    <div class="license-title">
      <div>redis面试题-2</div>
      <div>http://example.com/2022/04/03/redis面试题-2/</div>
    </div>
    <div class="license-meta">
      
        <div class="license-meta-item">
          <div>作者</div>
          <div>叶飞</div>
        </div>
      
      
        <div class="license-meta-item license-meta-date">
          <div>发布于</div>
          <div>2022年4月3日</div>
        </div>
      
      
      <div class="license-meta-item">
        <div>许可协议</div>
        <div>
          
            
            
              <a target="_blank" href="https://creativecommons.org/licenses/by/4.0/">
              <span class="hint--top hint--rounded" aria-label="BY - 署名">
                <i class="iconfont icon-by"></i>
              </span>
              </a>
            
          
        </div>
      </div>
    </div>
    <div class="license-icon iconfont"></div>
  </div>



              
                <div class="post-prevnext my-3">
                  <article class="post-prev col-6">
                    
                    
                      <a href="/2022/04/03/20220403-redis%E9%9D%A2%E8%AF%95%E9%A2%98-3/" title="redis面试题-3">
                        <i class="iconfont icon-arrowleft"></i>
                        <span class="hidden-mobile">redis面试题-3</span>
                        <span class="visible-mobile">上一篇</span>
                      </a>
                    
                  </article>
                  <article class="post-next col-6">
                    
                    
                      <a href="/2022/04/03/redis%E9%9D%A2%E8%AF%95%E9%A2%98-1/" title="redis面试题-1">
                        <span class="hidden-mobile">redis面试题-1</span>
                        <span class="visible-mobile">下一篇</span>
                        <i class="iconfont icon-arrowright"></i>
                      </a>
                    
                  </article>
                </div>
              
            </div>

            
          </article>
        </div>
      </div>
    </div>

    <div class="side-col d-none d-lg-block col-lg-2">
      
  <aside class="sidebar" style="margin-left: -1rem">
    <div id="toc">
  <p class="toc-header"><i class="iconfont icon-list"></i>&nbsp;目录</p>
  <div class="toc-body" id="toc-body"></div>
</div>



  </aside>


    </div>
  </div>
</div>





  



  



  



  



  


  
  









    

    
      <a id="scroll-top-button" aria-label="TOP" href="#" role="button">
        <i class="iconfont icon-arrowup" aria-hidden="true"></i>
      </a>
    

    
      <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v" for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>

    

    
  </main>

  <footer>
    <div class="footer-inner">
  
    <div class="footer-content">
       <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><span>Hexo</span></a> <i class="iconfont icon-love"></i> <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener"><span>Fluid</span></a> 
    </div>
  
  
  
  
</div>

  </footer>

  <!-- Scripts -->
  
  <script  src="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.js" ></script>
  <link  rel="stylesheet" href="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.css" />

  <script>
    NProgress.configure({"showSpinner":false,"trickleSpeed":100})
    NProgress.start()
    window.addEventListener('load', function() {
      NProgress.done();
    })
  </script>


<script  src="https://lib.baomitu.com/jquery/3.6.0/jquery.min.js" ></script>
<script  src="https://lib.baomitu.com/twitter-bootstrap/4.6.1/js/bootstrap.min.js" ></script>
<script  src="/js/events.js" ></script>
<script  src="/js/plugins.js" ></script>


  <script  src="https://lib.baomitu.com/typed.js/2.0.12/typed.min.js" ></script>
  <script>
    (function (window, document) {
      var typing = Fluid.plugins.typing;
      var subtitle = document.getElementById('subtitle');
      if (!subtitle || !typing) {
        return;
      }
      var text = subtitle.getAttribute('data-typed-text');
      
        typing(text);
      
    })(window, document);
  </script>




  
    <script  src="/js/img-lazyload.js" ></script>
  




  
<script>
  Fluid.utils.createScript('https://lib.baomitu.com/tocbot/4.18.2/tocbot.min.js', function() {
    var toc = jQuery('#toc');
    if (toc.length === 0 || !window.tocbot) { return; }
    var boardCtn = jQuery('#board-ctn');
    var boardTop = boardCtn.offset().top;

    window.tocbot.init({
      tocSelector     : '#toc-body',
      contentSelector : '.markdown-body',
      headingSelector : CONFIG.toc.headingSelector || 'h1,h2,h3,h4,h5,h6',
      linkClass       : 'tocbot-link',
      activeLinkClass : 'tocbot-active-link',
      listClass       : 'tocbot-list',
      isCollapsedClass: 'tocbot-is-collapsed',
      collapsibleClass: 'tocbot-is-collapsible',
      collapseDepth   : CONFIG.toc.collapseDepth || 0,
      scrollSmooth    : true,
      headingsOffset  : -boardTop
    });
    if (toc.find('.toc-list-item').length > 0) {
      toc.css('visibility', 'visible');
    }
  });
</script>


  <script src=https://lib.baomitu.com/clipboard.js/2.0.10/clipboard.min.js></script>

  <script>Fluid.plugins.codeWidget();</script>


  
<script>
  Fluid.utils.createScript('https://lib.baomitu.com/anchor-js/4.3.1/anchor.min.js', function() {
    window.anchors.options = {
      placement: CONFIG.anchorjs.placement,
      visible  : CONFIG.anchorjs.visible
    };
    if (CONFIG.anchorjs.icon) {
      window.anchors.options.icon = CONFIG.anchorjs.icon;
    }
    var el = (CONFIG.anchorjs.element || 'h1,h2,h3,h4,h5,h6').split(',');
    var res = [];
    for (var item of el) {
      res.push('.markdown-body > ' + item.trim());
    }
    if (CONFIG.anchorjs.placement === 'left') {
      window.anchors.options.class = 'anchorjs-link-left';
    }
    window.anchors.add(res.join(', '));
  });
</script>


  
<script>
  Fluid.utils.createScript('https://lib.baomitu.com/fancybox/3.5.7/jquery.fancybox.min.js', function() {
    Fluid.plugins.fancyBox();
  });
</script>


  <script>Fluid.plugins.imageCaption();</script>

  
      <script>
        if (!window.MathJax) {
          window.MathJax = {
            tex    : {
              inlineMath: { '[+]': [['$', '$']] }
            },
            loader : {
              load: ['ui/lazy']
            },
            options: {
              renderActions: {
                insertedScript: [200, () => {
                  document.querySelectorAll('mjx-container').forEach(node => {
                    let target = node.parentNode;
                    if (target.nodeName.toLowerCase() === 'li') {
                      target.parentNode.classList.add('has-jax');
                    }
                  });
                }, '', false]
              }
            }
          };
        } else {
          MathJax.startup.document.state(0);
          MathJax.texReset();
          MathJax.typeset();
          MathJax.typesetPromise();
        }
      </script>
    

  <script  src="https://lib.baomitu.com/mathjax/3.2.1/es5/tex-mml-chtml.js" ></script>

  <script  src="/js/local-search.js" ></script>

  <script defer src="/js/leancloud.js" ></script>





<!-- 主题的启动项，将它保持在最底部 -->
<!-- the boot of the theme, keep it at the bottom -->
<script  src="/js/boot.js" ></script>


  

  <noscript>
    <div class="noscript-warning">博客在允许 JavaScript 运行的环境下浏览效果更佳</div>
  </noscript>
<script src="/live2dw/lib/L2Dwidget.min.js?094cbace49a39548bed64abff5988b05"></script><script>L2Dwidget.init({"log":false,"pluginJsPath":"lib/","pluginModelPath":"assets/","pluginRootPath":"live2dw/","tagMode":false});</script></body>
</html>
